/**
 * Current version is without mirror agents, plan to turn it off
 */
package edu.rice.myxo.sim;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;

import edu.rice.myxo.utils.IOutils;
import edu.rice.myxo.utils.MathUtils;

/**
 * @author Haiyang Zhang
 * 
 */
public class MyxoMain {

	public static void main(String[] args) throws SecurityException,
			IllegalArgumentException, IOException, NoSuchFieldException,
			IllegalAccessException {
		new MyxoMain(args);
	}

	public MyxoMain(String[] args) throws SecurityException,
			IllegalArgumentException, IOException, NoSuchFieldException,
			IllegalAccessException {
	    System.out.println("Build Correct lalala!");
		iniSim(args);
		for (Global.currTime = Global.dT; Global.currTime <= Global.simTime; Global.currTime += Global.dT) {
			step();
			output();
		}
		finSim();
	}

	public void iniSim(String[] args) throws SecurityException,
			IllegalArgumentException, IOException, NoSuchFieldException,
			IllegalAccessException {

		System.out.println("Program initializing...");
		/*
		 * Initialize main fields
		 */
		statUtil = new Statistics();
		ioUtil = new IOutils();

		/*
		 * configFile is the configuration file for the simulation paramFile is
		 * for the parameter scan simulations
		 */
		String configFile;
		String cellDataFile;
		switch (args.length) {
		case 0:
			configFile = "config.txt";
			cellDataFile = null;
			break;
		case 1:
			configFile = args[0];
			cellDataFile = null;
			break;
		case 2:
			configFile = args[0];
			cellDataFile = args[1];
			break;
		default:
			configFile = "config.txt";
			cellDataFile = null;
			break;
		}
		Global.getParam(configFile);
		Global.getResettingMap("resettingmap.txt");
		Global.refineResettingMap(Global.resettingMap, Global.refineMapType);
		/*
		 * Create output file folder
		 */
		Global.dirName = "data";
		// Global.dirName = "data" + System.currentTimeMillis();
		Global.imageDirName = "image";
		// Global.imageDirName = "image" + System.currentTimeMillis();

		File dir = new File(Global.dirName);
		dir.mkdir();
		File dirImage = new File(Global.imageDirName);
		dirImage.mkdir();

		// Global.r.setSeed(System.currentTimeMillis());
		Global.r.setSeed(1234567);
		Global.writeParam(Global.dirName);
		ioUtil.writeArray(Global.resettingMap, Global.dirName, "resettingmap");

		/*
		 * Initiallize the mirror agentlist
		 */
		// Global.mirrorAgents = new ArrayList<MyxoAgent>();
		/*
		 * Initialize cells and agent space
		 */
		agentSpace = new AgentSpace();
		slimeSpace = new SlimeSpace();
		cSignalSpace = new ChemicalSpace(Global.cSiganlStartX,
				Global.cSignalStartY, Global.cSiganlWidth,
				Global.cSignalHeight, (int) Global.cSignalType);
		agentList = new ArrayList<MyxoAgent>();
		if (args.length < 2) {
			for (int i = 0; i < Global.cellNum; i++) {
				agentList.add(new MyxoAgent(agentSpace, slimeSpace,
						cSignalSpace, i));
			}
		} else {
			System.out.println(cellDataFile);
			File cellData = new File(cellDataFile);
			if (cellData.exists()) {
				Scanner sc = new Scanner(cellData);
				int i = 0;
				while (sc.hasNext()) {
					String dataline = sc.nextLine();
					if (dataline.contains("#")) {
						continue;
					}
					if (Global.DEBUG) {
						System.out.println(dataline);
					}
					agentList.add(new MyxoAgent(dataline, agentSpace,
							slimeSpace, cSignalSpace, i));
					i++;
				}
				Global.cellNum = i;
			} else {
				System.out.println("Cell data file does not exist!");
			}
		}
		if (Global.DEBUG) {
			for (int i = 0; i < agentList.size(); i++) {
				System.out.println("cell " + i + " has dir "
						+ agentList.get(i).getDir());
			}
		}
		agentSpace.setAgentList(agentList);
		edgeCellNum = (int) ((double) Global.cellNum * Global.edgeCellFraction) < (int) (Global.iniHeight / Global.cellWidth) ? (int) ((double) Global.cellNum * Global.edgeCellFraction)
				: (int) (Global.iniHeight / Global.cellWidth);
		edgeDistance = new double[edgeCellNum];

		// Initializing which cells signal is turned on
		// int[] allAgentIndex = new int[Global.cellNum];
		// MathUtils.shuffle(allAgentIndex);
		// int noSigNum = (int)((double)Global.cellNum * Global.noSigRatio);
		// System.arraycopy(allAgentIndex, 0, noSigIndex, 0, noSigNum);
		// System.arraycopy(allAgentIndex, noSigNum, sigIndex, 0,
		// (Global.cellNum - noSigNum));
		// /////////////////////////////////////////

		timeSteps = (int) (Global.simTime / Global.dT);
		Global.cellFluxArray = new int[timeSteps];
		Global.cellNumberArray = new int[timeSteps];
		Global.distanceExpan = new double[timeSteps];
		Global.signalTrack = new double[timeSteps];
		allTheta = new double[(int) (Global.simTime / Global.outputDt)];
		// Average velocity time series.
		Global.vArray = new double[timeSteps];
		int trackedTimeSteps = (int) ((Global.trackEndTime - Global.trackStartTime)
				/ Global.dT + 1);
		Global.meanSquareDisplace = new double[trackedTimeSteps];

		// Different time stage MSD
		int earlyTimeSteps = (int) (Global.simTime / (3 * Global.dT));
		int middleTimeSteps = (int) (Global.simTime / (3 * Global.dT));
		int lateTimeSteps = (int) (Global.simTime / (3 * Global.dT));
		Global.msdEarly = new double[earlyTimeSteps];
		Global.msdLate = new double[lateTimeSteps];
		Global.msdMiddle = new double[middleTimeSteps];
		order = new double[(int) (Global.simTime / Global.outputDt)];
		orie = new double[(int) (Global.simTime / Global.outputDt)];
		Global.reversalpoints = new ArrayList[30];
		for(int i = 0; i < Global.reversalpoints.length; i++){
		    Global.reversalpoints[i] = new ArrayList<double[]>();
		}
		timeStart = System.currentTimeMillis();
		System.out.println("Initialization finished. Simulation starting...");
	}

	public void step() {
		
	
		
		if (Global.currTime % (Global.simTime / 10) == 0) {
			Global.allAgents = new double[agentList.size()][7];
			for (int i = 0; i < agentList.size(); i++) {
				Global.allAgents[i] = new double[7];
			}
		}
		noSigNum = 0;
		timePrev = System.currentTimeMillis();
		Global.numCollisions = 0;
		// Global.mirrorAgents.clear();
		Global.timeIndex = (int) (Global.currTime / Global.dT - 1);
		meanSquareSum = 0;
		generalMSS = 0;
		constCellNum = 0;
		allDistance = new double[agentList.size()];
		if (Global.DEBUG) {
			System.out.println("Current time is " + Global.currTime);
		}
		// vAllcells = new double[agentList.size()];
		// xAllcells = new double[agentList.size()];
		// yAllcells = new double[agentList.size()];

		// Label and uunlabel cells; general label has 3 stages during
		// simulation
		// first 1/3 part, middle 1/3 part and end 1/3 part; label has 1 stage,
		// from
		// trackStartTime to trackEndTime
		if (Global.currTime == Global.trackStartTime) {
			labelCells();
		}
		if (Global.currTime == Global.trackEndTime) {
			unLabelCells();
		}
		if (Global.currTime == Global.dT) {
			labeledCellNum = generalLabelCells(agentList, Global.labeledCellNum);
		}
		if (Global.currTime == Global.simTime / 3.0) {
			generalUnlabelcells();
			labeledCellNum = generalLabelCells(agentList, Global.labeledCellNum);
		}
		if (Global.currTime == 2 * Global.simTime / 3) {
			generalUnlabelcells();
			labeledCellNum = generalLabelCells(agentList, Global.labeledCellNum);
		}

		Global.localDensity = 0;
		Global.cellpaircount = 0;

		/*
		int threadN = (int) Global.threadN;
		Workers[] w = new Workers[threadN];
		Global.cpcparts = new double[threadN];
		Global.ovs = new double[threadN];
		Global.gos = new double[threadN];
		Global.ocs = new double[threadN];
		for (int i = 0; i < threadN; i++) {
			w[i] = new Workers(agentList, i * agentList.size() / threadN,
					(i + 1) * agentList.size() / threadN, i);
			w[i].start();
		}
		for (int i = 0; i < threadN; i++) {
			try {
				w[i].join();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}

		if (((int) Global.currTime) % ((int) Global.outputDt) == 0) {
			for (int i = 0; i < threadN; i++) {
				order[(int) (Global.currTime / Global.outputDt) - 1] += Global.gos[i];
				orie[(int) (Global.currTime / Global.outputDt) - 1] += Global.ocs[i];
				allTheta[(int) (Global.currTime / Global.outputDt) - 1] += Global.ovs[i];
				Global.cellpaircount += Global.cpcparts[i];
			}
		}
		*/
		
		//This loop goes trough each agent once in one thread
		//No computing intensive code should be included in this loop
		for (int i = 0; i < agentList.size(); i++) {
			if (Global.DEBUG) {
				System.out.println("Simulating cell " + i + " with dir "
						+ agentList.get(i).getDir() + " and phase "
						+ agentList.get(i).getPhase());
			}

			// Updating each agent
		   agentList.get(i).step();
			// if (((int) Global.currTime) % ((int) Global.outputDt) == 0) {
			// if (i % 100 == 0) {
			// order[(int) (Global.currTime / Global.outputDt) - 1] += agentList
			// .get(i).calculateLocalOrder();
			// }
			// orie[(int) (Global.currTime / Global.outputDt) - 1] += agentList
			// .get(i).oriecorr();
			// allTheta[(int) (Global.currTime / Global.outputDt) - 1] +=
			// agentList
			// .get(i).orientationVariation();
			// }

			if (!agentList.get(i).isSIGNAL()) {
				noSigNum++;
			}
			// double dx = agentList.get(i).getX() - agentList.get(i).getxOld();
			// if(Math.abs(dx) > Global.worldX/2){
			// dx = Math.abs(dx) - Global.worldX;
			// }
			// double dy = agentList.get(i).getY() - agentList.get(i).getYOld();
			// if(Math.abs(dy) > Global.worldY/2){
			// dy = Math.abs(dy) - Global.worldY;
			// }
			// vAllcells[i] = Math.sqrt(dx*dx + dy * dy)/Global.dT;

			// xAllcells[i] = agentList.get(i).getXTrue();
			// yAllcells[i] = agentList.get(i).getYTrue();

			// Count colliding cells
			if (Global.DEBUG) {
				if (agentList.get(i).isCOLLIDE()) {
					System.out.println("Cell " + i + " is COLLDING");
				}
			}

			// Consider out of field cells
			if (agentList.get(i).isIFDROPPED()) {
				agentList.remove(i);
				continue;
			}

			// Consider constant cell density region
			if (agentList.get(i).isINCONSTDENRANGE()) {
				constCellNum++;
			}

			// Calculating mean square sum
			if (agentList.get(i).isTrackLabel()) {
				meanSquareSum += (agentList.get(i).getXDisFromMean() * agentList
						.get(i).getXDisFromIni() /*
												 * + agentList.get(i)
												 * .getyDisFromMean()
												 * agentList.get
												 * (i).getYDisFromMean()
												 */);
			}

			if (agentList.get(i).isGeneralTrack()) {
				generalMSS += agentList.get(i).getGeneralXDisFromMean()
						* agentList.get(i).getGeneralXDisFromMean();
			}

			// Consider outer edge position, expansion distance
			// if (edgeCellNum > 0) {
			// fillEdgeDistance(i);
			// }
			allDistance[i] = agentList.get(i).getX();

			// // Cell flux
			// if (Global.fluxType > 0) {
			// if (agentList.get(i).getFlagCrossBoundary() == 1) {
			// Global.cellFluxArray[Global.timeIndex] += 1;
			// agentList.get(i).setFlagCrossBoundary(0);
			// }
			// if (agentList.get(i).getFlagCrossBoundary() == 2) {
			// Global.cellFluxArray[Global.timeIndex] -= 1;
			// agentList.get(i).setFlagCrossBoundary(0);
			// }
			// }
		}

		if (((int) Global.currTime) % ((int) Global.outputDt) == 0) {
			order[(int) (Global.currTime / Global.outputDt) - 1] /= (double) agentList
					.size();
			order[(int) (Global.currTime / Global.outputDt) - 1] *= 100;
			orie[(int) (Global.currTime / Global.outputDt) - 1] /= Global.cellpaircount;
			allTheta[(int) (Global.currTime / Global.outputDt) - 1] /= (double) agentList
					.size();
		}
		// stats();
		if (Global.initialType > 1) {
			keepConstCellNum(Global.cellNum);
			Global.cellNumberArray[Global.timeIndex] = agentList.size();
		}
		// Debug information
		if (Global.DEBUG) {
			System.out.println("Total cell number is " + agentList.size());
			System.out.println("Current time flux "
					+ Global.cellFluxArray[Global.timeIndex]);
			for (int i = 0; i < Global.reverPeriods.size(); i++) {
				System.out.println(Global.reverPeriods.get(i));
			}
		}

		if (Global.currTime % (Global.simTime / 10) == 0) {
			ioUtil.writeMatrix(Global.allAgents, Global.dirName, "allagents");
		}

		System.out.println("average local density = " + Global.localDensity
				/ (double) Global.cellNum);
		agentSpace.updateAgentSpace();
		timeCurr = System.currentTimeMillis();
	}

	public void stats() {
		// mean square of cell displacement from mean position in past
		// In particular time, do statistics
		if (Global.currTime > Global.trackStartTime
				&& Global.currTime < Global.trackEndTime) {
			Global.meanSquareDisplace[Global.timeIndex
					- (int) (Global.trackStartTime / Global.dT)] = (double) meanSquareSum
					/ (double) Global.labeledCellNum;
		}
		if (Global.currTime > Global.dT && Global.currTime < Global.simTime / 3) {
			Global.msdEarly[Global.timeIndex - (int) (Global.dT / Global.dT)] = (double) generalMSS
					/ (double) labeledCellNum;
		}
		if (Global.currTime > Global.simTime / 3
				&& Global.currTime < Global.simTime * 2 / 3) {
			Global.msdMiddle[Global.timeIndex
					- (int) ((Global.simTime / 3) / Global.dT)] = (double) generalMSS
					/ (double) labeledCellNum;
		}
		if (Global.currTime > Global.simTime * 2 / 3) {
			Global.msdLate[Global.timeIndex
					- (int) ((Global.simTime * 2 / 3) / Global.dT)] = (double) generalMSS
					/ (double) labeledCellNum;
		}

		fillEdgeDistance();
		double meanEdge = 0;
		for (int i = 1; i < edgeDistance.length; i++) {
			meanEdge += edgeDistance[i];
		}
		meanEdge /= (double) (edgeDistance.length - 1);
		// System.out.println(meanEdge);
		Global.distanceExpan[Global.timeIndex] = meanEdge;
		Global.signalTrack[Global.timeIndex] = agentList.get(0).getPhase();
		Global.vArray[Global.timeIndex] /= (double) agentList.size();
		writeStats();
	}

	public void labelCells() {
		int cellsLabeled = 0;
		for (int i = 0; i < agentList.size(); i++) {
			if (agentList.get(i).getX() > 0.35 * Global.worldX
					&& agentList.get(i).getX() < 0.65 * Global.worldX) {
				if (agentList.get(i).getY() > 0.35 * Global.worldY
						&& agentList.get(i).getY() < 0.65 * Global.worldY) {
					agentList.get(i).setTrackLabel(true);
					agentList.get(i).statsInit();
					cellsLabeled += 1;
				}
			}
			if (cellsLabeled >= Global.labeledCellNum) {
				break;
			}
		}
		Global.labeledCellNum = cellsLabeled < Global.labeledCellNum ? cellsLabeled
				: Global.labeledCellNum;
	}

	public int generalLabelCells(ArrayList<MyxoAgent> agentList,
			int labeledCellNum) {
		int cellsLabeled = 0;
		for (int i = 0; i < agentList.size(); i++) {
			if (agentList.get(i).getX() > 0.35 * Global.worldX
					&& agentList.get(i).getX() < 0.65 * Global.worldX) {
				if (agentList.get(i).getY() > 0.35 * Global.worldY
						&& agentList.get(i).getY() < 0.65 * Global.worldY) {
					agentList.get(i).setGeneralTrack(true);
					agentList.get(i).generalStatInit();
					cellsLabeled += 1;
				}
			}
			if (cellsLabeled >= labeledCellNum) {
				break;
			}
		}
		labeledCellNum = cellsLabeled < labeledCellNum ? cellsLabeled
				: labeledCellNum;
		return labeledCellNum;
	}

	public void unLabelCells() {
		for (int i = 0; i < agentList.size(); i++) {
			if (agentList.get(i).isTrackLabel()) {
				agentList.get(i).setTrackLabel(false);
			}
		}
	}

	public void generalUnlabelcells() {
		for (int i = 0; i < agentList.size(); i++) {
			if (agentList.get(i).isGeneralTrack()) {
				agentList.get(i).setGeneralTrack(false);
			}
		}
	}

	public void fillEdgeDistance(int cellId) {
		if (cellId < edgeCellNum) {
			edgeDistance[cellId] = agentList.get(cellId).getX();
		} else {
			Arrays.sort(edgeDistance);
			edgeDistance[0] = agentList.get(cellId).getX();
		}
	}

	public static void fillEdgeDistance() {
		Arrays.sort(allDistance);
		System.arraycopy(allDistance, 0, edgeDistance, 0, edgeCellNum);
	}

	public void keepConstCellNum(int cellNum) {
		while (cellNum > constCellNum) {
			agentList.add(new MyxoAgent(agentSpace, slimeSpace, cSignalSpace,
					agentList.size()));
			constCellNum++;
		}
	}

	public void output() throws IOException {
		boolean stdout = true;
		if (stdout) {
			System.out.println("Command line output at time "
					+ (int) Global.currTime + "s(" + Global.currTime * 100
					/ Global.simTime + "%):");
			if ((timeCurr - timeStart) / 1000.0 <= 60) {
				System.out.println("Total time elapsed: "
						+ ((timeCurr - timeStart) / 1000.0) + "s");
			} else if ((timeCurr - timeStart) / 1000.0 <= 3600.0) {
				int min = (int) ((timeCurr - timeStart) / 1000) / 60;
				int sec = (int) ((timeCurr - timeStart) / 1000) % 60;
				System.out.println("Total time elapsed: " + min + "mins" + sec
						+ "seconds");
			} else if ((timeCurr - timeStart) / 1000.0 <= 3600.0 * 24.0) {
				int hour = (int) ((timeCurr - timeStart) / 1000) / 3600;
				int min = ((int) ((timeCurr - timeStart) / 1000) - hour * 3600) / 60;
				int sec = (int) ((timeCurr - timeStart) / 1000) % 60;
				System.out.println("Total time elapsed: " + hour + "hrs" + min
						+ "mins" + sec + "seconds");
			} else {
				int day = (int) ((timeCurr - timeStart) / 1000) / (24 * 3600);
				int hour = ((int) ((timeCurr - timeStart) / 1000) - 24 * 3600 * day) / 3600;
				int min = ((int) ((timeCurr - timeStart) / 1000) - day * 24
						* 3600 - hour * 3600) / 60;
				int sec = (int) ((timeCurr - timeStart) / 1000) % 60;
				System.out.println("Total time elapsed: " + day + "days" + hour
						+ "hrs" + min + "mins" + sec + "seconds");
			}
			System.out.println("Time elapse for this time step: "
					+ ((timeCurr - timePrev) / 1000.0) + "s");
			runtime = Runtime.getRuntime();
			System.out.println("Max memory" + runtime.maxMemory()
					/ (1024 * 1024) + "MB");
			System.out.println("Allocated memory" + runtime.totalMemory()
					/ (1024 * 1024) + "MB");
			System.out.println("Free memory" + runtime.freeMemory()
					/ (1024 * 1024) + "MB");
			// System.out.println("The p_group value of the system is " +
			// pGroup);
			System.out.println("Number of cells have cell-cell interaction: "
					+ Global.numCollisions);
			System.out.println("Percentage of nosignaling cell is: "
					+ ((double) noSigNum * 100.0 / (double) Global.cellNum)
					+ "%");
		}
		if ((int) Global.currTime % (int) Global.outputDt == 0) {

			agentSpace.outputAgentSpace(0, 0, Global.worldX, Global.worldY,
					Global.currTime);

			// simType = 1, cells have constant spatial speed; simType=2, cells
			// change speed according to cell density around
			switch ((int) Global.simType) {
			case 1:
				agentSpace.outputOneDirectionTravelingDensity();
				agentSpace.outputDensity(0, 0, Global.worldX, Global.worldY);
				// agentSpace.outputDemo(0, 0, Global.worldX, Global.worldY,
				// Global.currTime);
				break;
			case 2:
				agentSpace.outputDensity(0, 0, Global.worldX, Global.worldY);
				break;
			default:
				agentSpace.outputOneDirectionTravelingDensity();
				break;
			}
			writeStats();
		}
	}

	public void finSim() {
		timeEnd = System.currentTimeMillis();
		writeStats();
		System.out.println("Simulation has finished...");
	}

	public void writeStats() {
		ioUtil.writeList(Global.reverPeriods, Global.dirName, "reverperiods");
		ioUtil.writeList(Global.distancePerReverse, Global.dirName,
				"distanceperreverse");
		ioUtil.writePartialArray(Global.meanSquareDisplace, Global.dirName,
				"meansquaredisplacement", 1, 0);
		ioUtil.writePartialArray(Global.msdEarly, Global.dirName,
				"Early_MeanSquare", 1, 0);
		ioUtil.writePartialArray(Global.msdMiddle, Global.dirName,
				"Middle_MeanSquare", 1, 0);
		ioUtil.writePartialArray(Global.msdLate, Global.dirName,
				"Late_MeanSquare", 1, 0);
		ioUtil
				.writeList(Global.refractorTime, Global.dirName,
						"refractorytime");
		ioUtil.writePartialArray(Global.distanceExpan, Global.dirName,
				"expansiondistance", 1, 0);
		ioUtil.writeArray(Global.cellFluxArray, Global.dirName, "cellflux");
		ioUtil.writePartialArray(Global.cellNumberArray, Global.dirName,
				"totalcellnumber", 1, 0);
		
		// ioUtil.writeArray(Global.signalTrack, Global.dirName, "phase");
		// ioUtil.writeArray(Global.vArray, Global.dirName, "averagespeed");
		ioUtil.writeArray(allTheta, Global.dirName, "dirs");
		// String fn = "v" + Global.currTime;
		// ioUtil.writeArray(vAllcells, Global.dirName, fn);
		// String fnx = "x" + Global.currTime;
		// String fny = "y" + Global.currTime;
		// ioUtil.writeArray(xAllcells, Global.dirName, fnx);
		// ioUtil.writeArray(yAllcells, Global.dirName, fny);

		ioUtil.writeArray(order, Global.dirName, "globalorder");

		ioUtil.writeArray(orie, Global.dirName, "oriecorr");
	}

	private static long timeStart;

	private static long timeEnd;

	private static long timePrev;

	private static long timeCurr;

	private static int timeSteps;

	private static int constCellNum;

	private static int edgeCellNum;

	private static int labeledCellNum;

	private static int noSigNum;

	private static double[] edgeDistance;

	private static double[] allDistance;

	private static double[] allTheta;

	private static double[] vAllcells;

	private static double[] xAllcells;

	private static double[] yAllcells;

	private static int[] noSigIndex;

	private static int[] sigIndex;

	private static double meanSquareSum;

	private static double generalMSS;

	private static double[] order;

	private static double[] orie;

	private static Runtime runtime;

	private static AgentSpace agentSpace;

	private static SlimeSpace slimeSpace;

	private static ChemicalSpace cSignalSpace;

	private static ArrayList<MyxoAgent> agentList;

	private static Statistics statUtil;

	private static IOutils ioUtil;
}
